#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#include "display.h"

enum CHUNK_TYPE {
	CHUNK_INIT_AUDIO = 0,
	CHUNK_AUDIO = 1,
	CHUNK_INIT_VIDEO = 2,
	CHUNK_VIDEO = 3,
	CHUNK_SHUTDOWN = 4,
	CHUNK_END = 5,
};

enum OP_TYPE {
	OP_EOS = 0x0,
	OP_EOC = 0x1,
	OP_CREATE_TIMER = 0x02,
	OP_INIT_AUDIO_BUFFER = 0x03,
	OP_START_STOP_AUDIO = 0x04,
	OP_INIT_VIDEO_BUFFERS = 0x05,
	OP_VIDEO_UNCOMPRESSED = 0x06,
	OP_DISPLAY = 0x07,
	OP_AUDIO_FRAME = 0x08,
	OP_AUDIO_FRAME_SILENCE = 0x09,
	OP_INIT_VIDEO_MODE = 0xa,
	OP_CREATE_GRADIENT = 0xb,
	OP_SET_PALETTE = 0xc,
	OP_SET_PALETTE_COMPRESSED = 0xd,
	OP_SET_UNK_MAP = 0xe,
	OP_SET_DECODE_MAP = 0xf,
	OP_UNK3 = 0x10,
	OP_VIDEO_DATA = 0x11,
	OP_UNK4 = 0x12,
	OP_UNK5 = 0x13,
	OP_UNK6 = 0x14,
	OP_UNK7 = 0x15
};

typedef struct {
	unsigned int size;
	short unk1;
	short unk2;
	short unk3;
	char* data;
	unsigned int loc;
} video_data;

video_data data;
char *video_buffer;
char *video_buffer_cur;
char *video_buffer_prev;
char *video_buffer_debug;
char *video_buffer_debug2;
char *changed_blocks;

int movie_width;
int movie_height;
int delay;
int video_init = 0;
short pixel_width = 8;

short* unk_map;
int unk_map_size;

short* decode_map;
int decode_map_size;

int total_frames = 0;

char read_char() {
	char ret;
	memcpy(&ret, data.data + data.loc, 1);
	data.loc += 1;
	return ret;
} 

short read_int16() {
	short ret;
	memcpy(&ret, data.data + data.loc, 2);
	data.loc += 2;
	return ret;
} 

int read_int32() {
	int ret;
	memcpy(&ret, data.data + data.loc, 4);
	data.loc += 4;
	return ret;
} 

void swap_buffers() {
	printf("Swap buffers\n");
	char* c = video_buffer_cur;
	video_buffer_cur = video_buffer_prev;
	video_buffer_prev = c;

	//memset(video_buffer_cur, 0x33, movie_width * movie_height);
	//memcpy(video_buffer_cur, video_buffer_prev, movie_width * movie_height);
}

void check_header() {
	char magic[] = { 'I', 'n', 't', 'e', 'r', 'p', 'l', 'a', 'y', ' ', 'M', 'V', 'E', ' ', 'F', 'i', 'l', 'e', 0x1A, 0 };

	if (data.size < sizeof(magic)) {
		fprintf(stderr, "Wrong size, not an MVE file\n");
		exit(1);
	}
	
	if (memcmp(data.data, magic, sizeof(magic))) {
		fprintf(stderr, "Unknown file magic\n");
		exit(1);
	}

	data.loc = sizeof(magic);

	// And now we just assume we have data. This could be better
	data.unk1 = read_int16();
	data.unk2 = read_int16();
	data.unk3 = read_int16();

	printf("Found Interplay MVE file: Vermagic: 0x%04X 0x%04X 0x%04X\n", data.unk1, data.unk2, data.unk3);
}

void parse_chunk() {
	while(1) {
		int opsize = read_int16();
		char op = read_char();
		char ver = read_char();

		int data_loc = data.loc;
		printf("Opsize: %i, code: 0x%04X, version: %i\n\t", opsize, op, ver);

		switch(op) {
			case OP_EOS: {
				printf("OP_EOS\n");
				return;
			}
		
			case OP_EOC: {
				printf("OP_EOC\n\n");
				return;
			}

			case OP_CREATE_TIMER: {
				int rate = read_int32();
				short subdiv = read_int16();
				delay = (rate * subdiv + 500) / 1000;
				printf("OP_CREATE_TIMER: rate %i, subdiv %i, setting delay to %i\n", rate, subdiv, delay);
				break;
			}

			case OP_INIT_AUDIO_BUFFER: {
				short unknown = read_int16();
				short flags = read_int16();
				short rate = read_int16();
				int buflen;

				if (ver == 0)
					buflen = read_int16();
				if (ver == 1)
					buflen = read_int32();

				printf("OP_INIT_AUDIO_BUFFER v%i: unk 0x%04X, flags 0x%04X, rate %i, buflen %i\n", ver, unknown, flags, rate, buflen);
				break;
			}

			case OP_START_STOP_AUDIO: {
				printf("OP_START_STOP_AUDIO\n");
				break;
			}

			case OP_INIT_VIDEO_BUFFERS: {
				short width = read_int16();
				short height = read_int16();
				short count = 1;
				short true_color = 0;

				if (ver < 0 || ver > 2) {
					fprintf(stderr, "OP_INIT_VIDEO_BUFFERS: Unknown version\n");
					exit(1);
				}

				if (ver >=1)
					count = read_int16();
				if (ver == 2)
					true_color = read_int16();

				printf("OP_INIT_VIDEO_BUFFERS v%i: width %i, height %i, count %i, true_color %i\n", ver, width, height, count, true_color);

				movie_width = width * 8;
				movie_height = height * 8;
				video_buffer = calloc(movie_width * movie_height * 2, 1);
				video_buffer_cur = video_buffer;
				video_buffer_prev = video_buffer + movie_width * movie_height;
				video_buffer_debug = calloc(movie_width * movie_height, 1);
				video_buffer_debug2 = calloc(movie_width * movie_height, 1);

				// TODO
				memset(video_buffer_cur, 0xee, movie_width * movie_height);
				memset(video_buffer_prev, 0x33, movie_width * movie_height);
				memset(video_buffer_debug, 0xee, movie_width * movie_height);
				memset(video_buffer_debug2, 0x22, movie_width * movie_height);

				changed_blocks = calloc(width * height, 1);

				if (true_color)
					pixel_width = 16;
				break;
			}

			case OP_VIDEO_UNCOMPRESSED: {
				short frameno = read_int16(); // Seems to increase every frame anyway, starts at 1?
				short frameno2 = read_int16(); // Also increases every frame, starts at 0?
				
				short scale_width = read_int16(); // +4
				short scale_height = read_int16(); // +6
				short blocks_wide = read_int16(); // +8
				short blocks_high = read_int16(); // +10
				short unk1 = read_int16(); // +12

				printf("OP_VIDEO_UNCOMPRESSED: frameno %02i, frameno2 %02i, scale_width 0x%04X, scale_height 0x%04X, blocks wide %i, blocks high %i, bytes data %i, unk1: %04X\n", frameno, frameno2, scale_width, scale_height, blocks_wide, blocks_high, opsize - 12, unk1);;

				if(unk1 & 1) swap_buffers();

				short* metadata = (short*)(data.data + data.loc);

				char* pixeldata = data.data + data.loc + (blocks_wide * blocks_high * 2);
				char* out = video_buffer_cur;

				for (int block_y = 0; block_y < blocks_high; ++block_y) {
					for (int block_x = 0; block_x < blocks_wide; ++block_x) {
						if(! *metadata) {
							for (int i = 0; i < 8; ++i) {
								memcpy(out + (i * (blocks_wide * 8)), pixeldata, 8);
								pixeldata += 8;
							}
						}

						out += 8;
						metadata++;
					}

					out += (blocks_wide * 8) * 7;
				}

				out = video_buffer_cur;
				metadata = (short*)(data.data + data.loc);

				for (int block_y = 0; block_y < blocks_high; ++block_y) {
					for (int block_x = 0; block_x < blocks_wide; ++block_x) {
						short op = *metadata;
						if (op) {
							if(op < 0) {
								pixeldata = video_buffer_prev + (out - video_buffer_cur) + (unsigned short)op - 0xC000;
							} else {
								pixeldata = out + (unsigned short) op - 0x4000;
							}

							for (int i = 0; i < 8; ++i) {
								memcpy(out + (i * (blocks_wide * 8)), pixeldata + (i * (blocks_wide * 8)), 8);
							}
						}

						out += 8;
						++metadata;
					}
					out += (blocks_wide * 8) * 7;
				}

				break;	
			}

			case OP_DISPLAY: {
				short pal_start = read_int16(); // +0
				short pal_count = read_int16(); // +2

				if (ver == 0)
					printf("OP_DISPLAY v0: pal_start %i, pal_count %i\n", pal_start, pal_count);

				if (ver == 1) {
					short unk1 = read_int16(); // +4

					printf("OP_DISPLAY v1: pal_start %i, pal_count %i, unk1 0x%04X\n", pal_start, pal_count, unk1);
				}

				display_buffer(movie_width, movie_height, video_buffer_cur, video_buffer_prev, delay, changed_blocks);
				//display_buffer(movie_width * 2, movie_height, video_buffer, delay);
				//display_buffer(movie_width * 2, movie_height, video_buffer, 0);
				
				++total_frames;
				break;
			}

			case OP_AUDIO_FRAME: {
				short seq_index = read_int16();
				short stream_mask = read_int16();
				short stream_len = read_int16();

				printf("OP_AUDIO_FRAME: seq_index %i, stream_mask 0x%04X, stream_len %i\n", seq_index, stream_mask, stream_len);
				break;
			}

			case OP_AUDIO_FRAME_SILENCE: {
				short seq_index = read_int16();
				short stream_mask = read_int16();
				short stream_len = read_int16();

				printf("OP_AUDIO_FRAME_SILENCE: seq_index %i, stream_mask 0x%04X, stream_len %i\n", seq_index, stream_mask, stream_len);
				break;
			}

			case OP_INIT_VIDEO_MODE: {
				short width = read_int16();
				short height = read_int16();
				short flags = read_int16();
			
				printf("OP_INIT_VIDEO_MODE: width %i, height %i, flags 0x%04X\n", width, height, flags);
				if(!video_init) {
					create_window(width * 2, height * 2);
					wait_for_keypress();
					video_init = 1;
				}
				break;
			}

			case OP_CREATE_GRADIENT: {
				printf("OP_CREATE_GRADIENT\n");

				break;
			}

			case OP_SET_PALETTE: {
				unsigned char pal_start = read_int16();
				unsigned char pal_count = read_int16();

				/* 6-bit palette, convert to 8 bit */
				for (int i = 0; i < pal_count; ++i) {
					short r = read_char();
					short g = read_char();
					short b = read_char();
					colors[pal_start + i].r = (r << 2) | (r >> 4);
					colors[pal_start + i].g = (g << 2) | (g >> 4);
					colors[pal_start + i].b = (b << 2) | (b >> 4);
				}

				printf("OP_SET_PALETTE: pal_start %i, pal_count %i\n", pal_start, pal_count);
				break;
			}

			case OP_SET_PALETTE_COMPRESSED: {
				printf("OP_SET_PALETTE_COMPRESSED\n");

				break;
			}

			case OP_SET_UNK_MAP: {
				unk_map = (short*)(data.data + data.loc);
				unk_map_size = opsize / 2;

				printf("OP_SET_UNK_MAP: entries %i\n", unk_map_size);
				break;
			}

			case OP_SET_DECODE_MAP: {
				decode_map = (short*)(data.data + data.loc);
				decode_map_size = opsize / 2;

				printf("OP_SET_DECODE_MAP: entries %i\n", decode_map_size);
				break;
			}

			case OP_UNK3: {
				short frameno = read_int16(); // Seems to increase every frame anyway, starts at 1?
				short frameno2 = read_int16(); // Also increases every frame, starts at 0?
				
				short scale_width = read_int16(); // +4
				short scale_height = read_int16(); // +6
				short blocks_wide = read_int16(); // +8
				short blocks_high = read_int16(); // +10
				unsigned short unk1 = read_int16(); // +12

				printf("OP_UNK3: frameno %02i, frameno2 %02i, scale_width 0x%04X, scale_height 0x%04X, blocks wide %i, blocks high %i, unk1 %04X, bytes data %i\n", frameno, frameno2, scale_width, scale_height, blocks_wide, blocks_high, unk1, opsize - 14);

				if(unk1 & 1) swap_buffers();

				int source_blocks = 0;

				char* pixeldata = data.data + data.loc;
				char* out = video_buffer_cur;
				short* unk_map_l = unk_map;
				short* decode_map_l = decode_map;
				short cur_unk = *unk_map;
				char* changed_blocks_p = changed_blocks;
				memset(changed_blocks, 0, blocks_high * blocks_wide);

				for (int i = 0; i < unk_map_size; ++i) {
					printf("%hi, ", unk_map[i]);
				}
				printf("\n");

				for (int block_y = 0; block_y < blocks_high; ++block_y) {
					for (int block_x = 0; block_x < blocks_wide; ++block_x) {
						while (cur_unk <= 0)  {
							if (cur_unk != -0x8000 && cur_unk != 0) {
								if(! *decode_map_l) {
									for (int i = 0; i < 8; ++i) {
										memcpy(out + (i * (blocks_wide * 8)), pixeldata, 8);
										pixeldata += 8;
									}
									++source_blocks;
								} 
								*changed_blocks_p = 1;
								++decode_map_l;
								break;
							}
							cur_unk = *(++unk_map_l);
						}
						cur_unk *= 2;
						out += 8;
						++changed_blocks_p;
					}
					out += (blocks_wide * 8) * 7;
				}

				printf("Consumed %li out of %i unk_map entries, %i blocks copied\n", unk_map_l - unk_map + 1, unk_map_size, source_blocks);

				int cur_blocks = 0;
				int prev_blocks = 0;

				out = video_buffer_cur;
				unk_map_l = unk_map;
				decode_map_l = decode_map;
				cur_unk = *unk_map;

				for (int block_y = 0; block_y < blocks_high; ++block_y) {
					for (int block_x = 0; block_x < blocks_wide; ++block_x) {
						while (cur_unk <= 0) {
							if (cur_unk != -0x8000 && cur_unk != 0) {
								short op = *decode_map_l;
								if(op) {
									if(op < 0) {
										pixeldata = video_buffer_prev + (out - video_buffer_cur) + (unsigned short)op - 0xC000;
										++prev_blocks;
									} else {
										pixeldata = out + (op - 0x4000);
										++cur_blocks;
									}

									for (int i = 0; i < 8; ++i) {
										memcpy(out + (i * (blocks_wide * 8)), pixeldata + (i * (blocks_wide * 8)), 8);
									}
								}
								++decode_map_l;
								break;
							} 
							cur_unk = *(++unk_map_l);
						}
						cur_unk *= 2;
						out += 8;
					}
					out += (blocks_wide * 8) * 7;
				}

				printf("Consumed %li out of %i unk_map entries, %i prev blocks, %i cur blocks\n", unk_map_l - unk_map + 1, unk_map_size, prev_blocks, cur_blocks);
				break;
			}

			case OP_VIDEO_DATA: {
				printf("OP_VIDEO_DATA\n");

				break;
			}

			case OP_UNK4: {
				printf("OP_UNK4\n");

				break;
			}

			case OP_UNK5: {
				printf("OP_UNK5\n");

				break;
			}

			case OP_UNK6: {
				printf("OP_UNK6\n");

				break;
			}

			case OP_UNK7: {
				printf("OP_UNK7\n");

				break;
			}

			default:
				fprintf(stderr, "Unkown opcode 0x%04X\n", op);
		}

		data.loc = data_loc + opsize;
	}
}

short read_chunk() {
	short size = read_int16();
	short type = read_int16();

	printf("Found chunk type: ");
	switch(type) {
		case CHUNK_INIT_AUDIO:
			printf("INIT AUDIO ");
			break;
		case CHUNK_AUDIO:
			printf("AUDIO ");
			break;
		case CHUNK_INIT_VIDEO:
			printf("INIT VIDEO ");
			break;
		case CHUNK_VIDEO:
			printf("VIDEO ");
			break;
		case CHUNK_SHUTDOWN:
			printf("SHUTDOWN ");
			break;
		case CHUNK_END:
			printf("END ");
			break;
		default:
			printf("UNKNOWN! ");
			break;
	}

	printf("(0x%04X) of size %i\n", type, size);
	parse_chunk(size);
	
	return type;
}

void read_all_chunks() {
	while (1) {
		short t = read_chunk();
		if (t < 0 || t > CHUNK_VIDEO) {
			printf("End parsing\n");
			break;
		}
	}
}

int main(int argc, char *argv[]) {
	if(argc != 2) {
		fprintf(stderr, "Usage: %s [FILE]\n", argv[0]);
		exit(1);
	}

	init_sdl();

	FILE *f = fopen(argv[1], "rb");
	if (!f) {
		perror("Failed to open file");
		exit(1);
	}

	fseek(f, 0L, SEEK_END);
	data.size = ftell(f);
	rewind(f);

	data.data = malloc(data.size);
	fread(data.data, data.size, 1, f);
	fclose(f);

	data.loc = 0;

	check_header();
	read_all_chunks();
	
	wait_for_keypress();
	free(data.data);
	printf("Played %i frames\n", total_frames);
}
